package gu.sql2java;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

import com.google.common.base.Throwables;
import com.google.common.collect.Maps;

import gu.sql2java.Managers;
import gu.sql2java.ScriptRunner;
import gu.sql2java.Constant.JdbcProperty;

import static com.google.common.base.Throwables.throwIfInstanceOf;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static gu.sql2java.Constant.JdbcProperty.*;
import static gu.sql2java.SimpleLog.log;


/**
 * 嵌入式数据库初始化管理对象基类<br>
 * @author guyadong
 *
 */
public abstract class  BaseEmbeddedInitializer implements Closeable{
	static final int DEFAULT_BACKUP_INTERVAL = 300;
	protected final boolean runInMemory;
	protected int backupIntervalSeconds = DEFAULT_BACKUP_INTERVAL;
	/**
	 * 数据库所在的文件夹
	 */
	protected final File dbroot;
	/**
	 * 数据库位置(文件或文件夹)
	 */
	protected final File db;

	protected final Properties dbprops = new Properties();
	/**
	 * 数据库持久化侦听器容器
	 */
	private final InterfaceContainer<BackupHook> backuphooks = new InterfaceContainer<BackupHook>(BackupHook.class);
	private final URL createSql;
	private long lastBackupBeginTimeMills = 0L;
	private long lastBackupEndTimeMills = 0L;
	private volatile boolean opened = false;
	/**
	 * 构造方法
	 * @param db 数据库位置
	 * @param createSql 数据库建表语句SQL文件位置,数据库存在时可为{@code null},当数据库文件不存在时就用到.
	 * @param runInMemory
	 */
	protected BaseEmbeddedInitializer(File db, URL createSql, boolean runInMemory) {
		try {
			this.db = checkNotNull(db,"db is null").getCanonicalFile();
		} catch (IOException e) {
			throw new IllegalArgumentException(e);
		}
		checkArgument(this.db.exists() || null != createSql ,"createSql must not be null if %s not exists",this.db );
		this.createSql = createSql;
		this.dbroot = this.db.getParentFile() ;		
		dbroot.mkdirs();
		this.runInMemory = runInMemory;
	}
	/**
	 * 将数据库回写到磁盘指定的目录
	 */
	protected abstract void doPersist() ;
	
	/**
	 * 写入数据连接参数
	 * @param dbprops 
	 */
	protected abstract void writeDbProps(Properties dbprops);
	
	/**
	 * 检查已经存在的数据库是否可用,不可用抛出{@link EmbeddedInitException}异常
	 * @param db 数据库位置
	 * @throws EmbeddedInitException 初始化异常
	 */
	protected abstract void checkExistsDatabse(File db) throws EmbeddedInitException;
	/**
	 * 子类可重写此方法实现内存运行时从磁盘文件恢复数据到内存
	 */
	protected void doInitMemory(){}
	
	/**
	 * 子类可重写此方法执行对SQL语句的归一化处理
	 * @param runner
	 * @return always runner
	 */
	protected ScriptRunner normalize(ScriptRunner runner){
		return runner;
	}
	/**
	 * 子类可重写此方法返回需要附加执行的SQL语句(每行一条语句),如果没有返回空表,不可返回{@code null}
	 * @param runner
	 * @throws SQLException 
	 */
	protected List<String> afterCreateTable(ScriptRunner runner) throws SQLException{
		return Collections.emptyList();
	}
	/**
	 * 初始化数据库
	 * @return 当前对象
	 * @throws EmbeddedInitException
	 */
	public synchronized BaseEmbeddedInitializer init() throws EmbeddedInitException {
		if(!opened){
			try{
				log("database location:[{}]", this.db.getAbsolutePath());	
				this.createManagerInstance();
				if (this.db.exists()) {
					checkExistsDatabse(db);
					if(runInMemory){
						doInitMemory();
					}
				} else  {
					// 如果没找到数据库文件，就新建数据库
					log("Initializing database {}...", db);
					ScriptRunner runner = new ScriptRunner(false, true)
							.setClearComment(true)
							.setAlias(dbprops.getProperty(JdbcProperty.ALIAS.key));
					normalize(runner).runScript(createSql.openStream());
					List<String> sqls = afterCreateTable(runner);
					if(!sqls.isEmpty()){
						runner.runScript(sqls);
					}
				} 
				if(runInMemory){
					startBackuper();
				}
			}catch (Exception e) {
				throwIfInstanceOf(e, EmbeddedInitException.class);
				throw new EmbeddedInitException(e);
			}
			opened = true;
		}
		return this;
	}

	@Override
	public synchronized void close() {
		if(opened){
			persist();
			opened = false;
		}
	}
	public InterfaceContainer<BackupHook> getBackuphookContainer() {
		return backuphooks;
	}

	/**
	 * @return 备份间隔时间(秒)
	 */
	public int getBackupIntervalSeconds() {
		return backupIntervalSeconds;
	}
	/**
	 * 设置备份间隔时间(秒)
	 * @param backupIntervalSeconds 小于等于0忽略
	 * @return 当前对象
	 */
	public BaseEmbeddedInitializer setBackupIntervalSeconds(int backupIntervalSeconds) {
		if(backupIntervalSeconds > 0){
			this.backupIntervalSeconds = backupIntervalSeconds;
		}
		return this;
	}
	public BaseEmbeddedInitializer addProperties(Properties properties) {
		if(null != properties ){
			for( String propertyName : properties.stringPropertyNames()){
				dbprops.setProperty(propertyName, properties.getProperty(propertyName));
			}
		}
		return this;
	}
	/**
	 * 将数据库回写到磁盘指定的目录
	 */
	private synchronized void persist() {
		lastBackupBeginTimeMills = System.currentTimeMillis();
		try {
			this.backuphooks.container.onPersistDB();
			if (this.runInMemory) {
				log("wirte in-memory database to {}", db.getAbsolutePath());
				doPersist();
				log("write back success");
			}
		} catch (Exception e) {
			log(e.getMessage(), e);
		} finally {
			lastBackupEndTimeMills = System.currentTimeMillis();
		}
	}
	
	/**
	 * 根据数据库配置参数创建数据库连接实例
	 */
	private void createManagerInstance() {
		writeDbProps(dbprops);
		checkState(null != dbprops.getProperty(JDBC_URL.key),"%s not defined",JDBC_URL.key);
		// 创建数据库连接实例
		Managers.createInstance(dbprops);
	}

	/**
	 * 启动备份线程 定期将内存中的数据回写到数据库进行备份 当derby数据库在内存运行时，还要进行derby数据库备份
	 * 
	 * @see #persist()
	 */
	private void startBackuper() {
		Thread t = new Thread("edb-backuper") {
			@Override
			public void run() {
				long intervalMills = TimeUnit.MILLISECONDS.convert(backupIntervalSeconds, TimeUnit.SECONDS);
				log("backup thread start ,interval(s) :{}", backupIntervalSeconds);
				try {
					//通过线程中断状态来决定是否结束循环
					while (true) {
						Thread.sleep(intervalMills);
						// 通过lastBackupBeginTimeMills是否小于 lastBackupEndTimeMills,来判断止次备份是否结束
						if (lastBackupBeginTimeMills < lastBackupEndTimeMills) {// 正常备份
							if ((System.currentTimeMillis() - lastBackupEndTimeMills) >= intervalMills)
								persist();
						} else if (0 == lastBackupBeginTimeMills && lastBackupBeginTimeMills == lastBackupEndTimeMills)// 第一次备份
							persist();
						else
							// 上次备份还没结束
							log("SKIP backup because of too short interval");
					}
				} catch (InterruptedException e) {
				} finally {
					log("{} end (结束)",Thread.currentThread().getName());
				}
			}
		};
		t.setDaemon(true);
		t.start();
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((db == null) ? 0 : db.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (!(obj instanceof BaseEmbeddedInitializer))
			return false;
		BaseEmbeddedInitializer other = (BaseEmbeddedInitializer) obj;
		if (db == null) {
			if (other.db != null)
				return false;
		} else if (!db.equals(other.db))
			return false;
		return true;
	}
	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append(getClass().getSimpleName() + " [runInMemory=");
		builder.append(runInMemory);
		builder.append(", opened=");
		builder.append(opened);
		builder.append(", db=");
		builder.append(db);
		builder.append(", dbprops=");
		builder.append(dbprops);
		builder.append(", createSql=");
		builder.append(createSql);
		builder.append("]");
		return builder.toString();
	}
	
	private static final Map<File, BaseEmbeddedInitializer> dbs = Maps.newHashMap();
	/**
	 * 数据库初始化<br>
	 * 反射方式实现构造子类对象并执行初始化({@link #init()}),如果之前已经创建过{@code db}指定的数据库实例,则返回已经创建的实例
	 * @param target 要构造的子类
	 * @param db 数据文件位置
	 * @param createSql 数据库建表语句(SQL)位置
	 * @param runInMemory 为{@code true}以内存方式运行
	 * @param properties 附加的配置参数
	 * @return T instance 
	 */
	@SuppressWarnings("unchecked")
	protected static synchronized <T extends BaseEmbeddedInitializer>T init(Class<T>target, File db, URL createSql, boolean runInMemory, Properties properties){
		
		try {
			if(dbs.containsKey(db)){
				return (T) dbs.get(db);
			}
			final T initializer = target.getConstructor(File.class, URL.class, boolean.class).newInstance(db, createSql, runInMemory);
			initializer.addProperties(properties).init();
			// JVM结束时自动执行关闭数据库
			Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
				@Override
				public void run() {
					initializer.close();
				}
			}, initializer.getClass().getSimpleName() + "-closer"));
			return initializer;
		} catch (InvocationTargetException  e) {
			Throwables.throwIfUnchecked(e.getTargetException());
			throw new RuntimeException(e.getTargetException());
		} catch ( Exception e) {
			Throwables.throwIfUnchecked(e);
			throw new RuntimeException(e);
		} 
	}
	/**
	 * 数据库初始化
	 * @param db 数据文件位置(File)
	 * @param createSqlURL 数据库建表语句(SQL)位置(URL)
	 * @param runInMemory 为{@code true}以内存方式运行
	 * @param properties 附加的配置参数
	 * @return T instance 
	 */
	protected static <T extends BaseEmbeddedInitializer>T init(Class<T>target, String db, String createSqlURL, boolean runInMemory, Properties properties){
		try {
			return init(target, new File(db), new URL(createSqlURL), runInMemory, properties);
		} catch (MalformedURLException e) {
			throw new RuntimeException(e);
		}
	}
}